| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362 |
- import Link from "next/link";
- import Head from "next/head";
- import { useMemo } from "react";
- import { useRouter } from "next/router";
- import TabUnstyled from "@mui/base/TabUnstyled";
- import TabsUnstyled from "@mui/base/TabsUnstyled";
- import TabsListUnstyled from "@mui/base/TabsListUnstyled";
- import TabPanelUnstyled from "@mui/base/TabPanelUnstyled";
- import type { GetServerSideProps, NextPage } from "next";
- import { get } from "libs/http";
- import MyError from "pages/_error";
- import useGet from "libs/hooks/useGet";
- import errorProps from "libs/errorProps";
- import useStore from "libs/hooks/useStore";
- import NovelCover from "components/NovelCover";
- import styles from "styles/novel-info.module.scss";
- import { SeoHead, SeoHeadConfig } from "components/SeoHead";
- interface NovelPageProps {
- detail?: Detail;
- chapters?: ChapterListData;
- statusCode?: number;
- }
- const Novel: NextPage<NovelPageProps> = (props) => {
- const { statusCode } = props;
- const { siteConfig } = useStore();
- const { query } = useRouter();
- const { data: { data: detail } = { data: null } } = useGet<Detail>(
- `/api/novel/${query.slug}`
- );
- const { data: { data: chapters } = { data: null } } = useGet<ChapterListData>(
- `/api/novel/${query.slug}/chapters`
- );
- const seoConfig: SeoHeadConfig = useMemo(() => {
- const keys = detail?.genres.map((item) => item.name).join(", ");
- return {
- title: `${detail?.name} - ${siteConfig.siteName}`,
- description: `${detail?.name} is ${keys} web novel. Read ${detail?.name} novel written by the author ${detail?.author} on ${siteConfig.siteName} for Free.`,
- keywords: `${[
- detail?.name,
- detail?.author,
- detail?.other_name,
- keys,
- siteConfig.keywords,
- siteConfig.siteName,
- ]
- .filter((item) => item)
- .join(", ")}`,
- url: `https://${siteConfig.host}/novel/${query.slug}`,
- siteName: siteConfig.siteName,
- img: detail?.img,
- jsonLd: JSON.stringify([
- {
- "@context": "https://schema.org",
- "@type": "Book",
- mainEntityOfPage: `https://${siteConfig.host}/novel/${query.slug}`,
- headline: detail?.name,
- name: detail?.name,
- genre: detail?.genres[0].name,
- image: {
- "@type": "ImageObject",
- url: detail?.img,
- },
- bookFormat: "https://schema.org/EBook",
- datePublished: detail?.create_time,
- dateModified: detail?.update_time,
- author: {
- "@type": "Person",
- name: detail?.author,
- },
- copyrightHolder: detail?.author,
- publisher: {
- "@type": "Organization",
- name: siteConfig.siteName,
- logo: {
- "@type": "ImageObject",
- url: `https://${siteConfig.host}/favicon-32x32.png`,
- },
- },
- description: detail?.desc,
- // aggregateRating: {
- // "@type": "AggregateRating",
- // bestRating: "5.0",
- // ratingValue: "4.62",
- // ratingCount: "159",
- // },
- potentialAction: {
- "@type": "ReadAction",
- target: {
- "@type": "EntryPoint",
- urlTemplate: `https://${siteConfig.host}/novel/${chapters?.chapters[0].uri}`,
- },
- },
- },
- {
- "@context": "https://schema.org",
- "@type": "BreadcrumbList",
- itemListElement: [
- {
- "@type": "ListItem",
- position: 1,
- name: "Home",
- item: `https://${siteConfig.host}`,
- },
- {
- "@type": "ListItem",
- position: 2,
- name: detail?.genres[0].name,
- item: `https://${siteConfig.host}/novels/${detail?.genres[0].uri}`,
- },
- {
- "@type": "ListItem",
- position: 3,
- name: `${detail?.name || ""}`,
- item: `https://${siteConfig.host}/novel/${query.slug}`,
- },
- ],
- },
- ...siteConfig.jsonLd,
- ]),
- };
- }, [
- chapters?.chapters,
- detail?.author,
- detail?.create_time,
- detail?.desc,
- detail?.genres,
- detail?.img,
- detail?.name,
- detail?.other_name,
- detail?.update_time,
- query.slug,
- siteConfig.host,
- siteConfig.jsonLd,
- siteConfig.keywords,
- siteConfig.siteName,
- ]);
- const chapterLists = useMemo(() => {
- if (!chapters || !chapters.chapters.length) return [];
- const len = Math.ceil(chapters.chapters.length / 100);
- const list = [];
- for (let i = 0; i < len; i++) {
- list.push({
- title: `${i * 100 + 1}-${(i + 1) * 100}`,
- list: chapters.chapters.slice(
- i * 100,
- Math.min(chapters.chapters.length, (i + 1) * 100)
- ),
- });
- }
- return list;
- }, [chapters]);
- if (statusCode) {
- return <MyError statusCode={statusCode} />;
- }
- if (!detail || !chapters) {
- return null;
- }
- return (
- <main>
- <SeoHead seoConfig={seoConfig} />
- <div
- className={styles["novel-wrap"]}
- style={{
- backgroundImage: `url(${detail?.img})`,
- }}
- >
- <div className={styles["novel-container"]}>
- <div className="text-sm py-4 breadcrumbs">
- <ul>
- <li>
- <Link href="/" title="Home">
- <svg className="w-5 h-5 mr-1 -mt-1">
- <use xlinkHref="/icons.svg#home"></use>
- </svg>
- Home
- </Link>
- </li>
- {detail.genres && detail.genres.length > 0 ? (
- <li>
- <Link
- title={detail.genres[0].name}
- href={`/novels/${detail.genres[0].uri}`}
- >
- {detail.genres[0].name}
- </Link>
- </li>
- ) : null}
- <li>{detail.name}</li>
- </ul>
- </div>
- <div className={styles["novel-info"]}>
- <NovelCover
- className={styles["novel-info-cover"]}
- component="div"
- alt={detail.name}
- src={detail.img}
- />
- <div className={styles["nove-info-body"]}>
- <h1>
- {detail.name}
- <small>Completed</small>
- </h1>
- <h2>
- {detail.genres && detail.genres.length > 0 ? (
- <Link
- title={detail.genres[0].name}
- href={`/novels/${detail.genres[0].uri}`}
- >
- <svg>
- <use xlinkHref="/icons.svg#paper"></use>
- </svg>
- <span>{detail.genres[0].name}</span>
- </Link>
- ) : null}
- <strong>
- <svg>
- <use xlinkHref="/icons.svg#chapter"></use>
- </svg>
- <span>{chapters.chapters.length} Chapters</span>
- </strong>
- {/* <strong>
- <svg>
- <use xlinkHref="/icons.svg#eye"></use>
- </svg>
- <span>0 Views</span>
- </strong> */}
- </h2>
- <div className={styles["btns"]}>
- <Link
- href={`/novel/${chapters.chapters[0].uri}`}
- className={styles["button"]}
- >
- Start Reading
- </Link>
- </div>
- </div>
- </div>
- </div>
- </div>
- <TabsUnstyled defaultValue={0} className="container bg-paper py-3">
- <TabsListUnstyled className="tabs">
- <TabUnstyled value={0} className="tab">
- About
- </TabUnstyled>
- <TabUnstyled value={1} className="tab">
- Chapters
- </TabUnstyled>
- </TabsListUnstyled>
- <TabPanelUnstyled value={0}>
- <h2 className="sub-title">Tags</h2>
- <div className="tags">
- {detail.genres.map((item) => (
- <Link
- title={item.name}
- href={`/novels/${item.uri}`}
- className="tag"
- key={item.uri}
- >
- {item.name}
- </Link>
- ))}
- </div>
- <h2 className="sub-title">Synopsis</h2>
- <div
- className={styles["novel-text"]}
- dangerouslySetInnerHTML={{ __html: detail.desc }}
- />
- </TabPanelUnstyled>
- <TabPanelUnstyled value={1}>
- <h3 className="sub-title">{detail.name} Chapters</h3>
- {/* {isServer ? (
- <ol className={styles["chapter-list"]}>
- {chapters.chapters.map((item) => (
- <li key={item.id}>
- <Link href={`/novel/${item.uri}`} title={item.name}>
- <i>1</i>
- <strong>{item.name}</strong>
- <small>1yr</small>
- </Link>
- </li>
- ))}
- </ol>
- ) : ( */}
- <TabsUnstyled defaultValue={0} className="container bg-paper py-3">
- <TabsListUnstyled className="tabs">
- {chapterLists.map((chapter, idx) => (
- <TabUnstyled value={idx} className="tab" key={chapter.title}>
- {chapter.title}
- </TabUnstyled>
- ))}
- </TabsListUnstyled>
- {chapterLists.map((chapter, idx) => (
- <TabPanelUnstyled
- value={idx}
- component="ol"
- key={chapter.title}
- className={styles["chapter-list"]}
- >
- {chapter.list.map((item) => (
- <li key={item.id}>
- <Link href={`/novel/${item.uri}`} title={item.name}>
- <i>1</i>
- <strong>{item.name}</strong>
- <small>1yr</small>
- </Link>
- </li>
- ))}
- </TabPanelUnstyled>
- ))}
- </TabsUnstyled>
- {/* )} */}
- </TabPanelUnstyled>
- </TabsUnstyled>
- </main>
- );
- };
- export const getServerSideProps: GetServerSideProps<
- Docs,
- { slug: string }
- > = async (context) => {
- if (!context.params) {
- return errorProps(context);
- }
- try {
- const { slug } = context.params;
- const [detail, chapters] = await Promise.all([
- get<Detail>(`/api/novel/${slug}`),
- get<ChapterListData>(`/api/novel/${slug}/chapters`),
- ]);
- if (!detail || !detail.data.id) {
- return errorProps(context);
- }
- return {
- props: {
- fallback: {
- [`/api/novel/${slug}`]: detail,
- [`/api/novel/${slug}/chapters`]: chapters,
- },
- },
- };
- } catch (e) {
- return errorProps(context, 500);
- }
- };
- export default Novel;
|